[Android] Android O 广播限制


问题

因为项目需要迁移到8.0平台,发现有一个系统应用打不开,从log发现如下描述:

BroadcastQueue: Background execution not allowed: receiving Intent {...}

实际上,在Android O,像下面的隐式广播都不再起作用:

sendBroadcast(new Intent("this.is.an.implicit.broadcast"));

通常来讲,这个广播会被所有注册这个action的receiver接收到。即便是在Android O版本,还有两类receiver仍然会接收这个广播:

  1. targetSdkVersion <= 25的应用
  2. 通过registerReceiver()注册的并有已经启动的进程的应用

但是通过Manifest文件静态注册的BroadcastReceiver是不会接收这个广播的,反而会打印出一些开头提到的log

W/BroadcastQueue: Background execution not allowed: receiving Intent { act=android.intent.action.PACKAGE_REMOVED dat=package:com.commonsware.cwac.cam2.demo flg=0x4000010 (has extras) } to com.commonsware.android.sysevents.pkg/.OnPackageChangeReceiver

症结

这个问题看起来可能与电池有关,因为自从Android 6.0中引入Doze模式以来,各种后台处理会引起类似现象。但事实上,电池问题是次要的,真正的原因是进程混乱。

对此谷歌工程师是这样反馈的:

为了帮助了解发生了什么,我需要澄清一下,此更改的目的并不直接与电池使用有关,而是要解决平台中长期存在的问题:处于内存压力下的设备可能会进入错误的内存抖动状态。这些状态通常是由于广播引起的:某些广播或广播的发送相对频繁,许多应用程序正在通过清单进行监听(因此需要启动以接收它),但是没有足够的RAM来保留所有状态这些应用程序的进程在缓存中进行处理,因此,每次发送广播时,系统最终都会不断地在各个进程中进行跳动。无论设备当前是否已接通电源,这都是一个问题。实际上,这在Android TV设备(始终插上电源)上可能经常会成为问题,因为它们的RAM往往很紧!

这就好理解了,尤其是很多开发者为了唤醒自己的app,注册了非常多的静态广播(我看过喜马拉雅注册了100多个静态广播,毫无下限),每当系统发送一个广播时,就会出现唤醒很多app的情况,又因为系统内存有限,启动一些app后另一些app又被杀掉。这样不仅耗电,还影响使用性能。

Android O 广播限制

如果应用注册了BroadcastReceiver,则每次发送广播的时候,应用的BroadcastReceiver都会消耗资源。如果多个应用注册了接收基于系统事件的广播,就会出现,触发广播的系统事件会导致所有应用快速的连续消耗资源,从而降低用户体验。为了缓解这个问题,Android N对广播施加了一些限制,而Android O让这些限制更加严格。

Android N做的限制

  • Android 7.0 及其更高版本不再接收 CONNECTIVITY_ACTION 的静态广播(在清单注册的receiver)。但是如果是通过 Context.registerReceiver() 注册的动态广播,相关BroadcastReceiver还是可以接收到 CONNECTIVITY_ACTION 广播。
  • 应用无法再发送或者接收 ACTION_NEW_PICTUREACTION_NEW_VIDEO 广播了。这项优化影响所有的App,不只是针对Android 7.0。

Android O进一步的限制

  • 在Android 8.0 或更高版本的应用无法继续在其AndroidManifest中为隐式广播注册BroadcastReceiver。 隐式广播是一种不专门针对该应用的广播。 例如,ACTION_PACKAGE_REPLACED 就是一种隐式广播,因为该广播将被发送给所有已注册侦听器,让后者知道设备上的某些软件包已被替换。 不过,ACTION_MY_PACKAGE_REPLACED 不是隐式广播,因为不管已为该广播注册侦听器的其他应用有多少,它都会只被发送给软件包已被替换的应用。

  • 应用可以继续在它们的清单中注册显式广播。

  • 应用可以在运行时使用 Context.registerReceiver() 为任意广播(不管是隐式还是显式)注册接收器。
  • 需要签名权限的广播不受此限制所限,因为这些广播只会发送到使用相同证书签名的应用,而不是发送到设备上的所有应用。

在许多情况下,之前注册隐式广播的应用使用 JobScheduler 作业可以获得类似的功能。 例如,一款社交照片应用可能需要不时地执行数据清理,并且倾向于在设备连接到充电器时执行此操作。 之前,应用已经在清单中为 ACTION_POWER_CONNECTED 注册了一个接收器;当应用接收到该广播时,它会检查清理是否必要。 为了迁移到 Android 8.0 或更高版本,应用将该接收器从其清单中移除。 应用将清理作业安排在设备处于空闲状态和充电时运行。

例外的隐式广播

很多隐式广播当前已不受此限制所限。 应用可以继续在其清单中为这些广播注册接收器,不管应用适配哪个 API 级别。

注意:即使这些隐式广播仍然可以在后台工作,但你应该尽量避免对它们注册监听。

  • ACTION_LOCKED_BOOT_COMPLETED,ACTION_BOOT_COMPLETED

    因为这些广播只在开机时发送一次,并且很多app需要接收这个广播来安排作业等操作。

  • ACTION_USER_INITIALIZE,”android.intent.action.USER_ADDED”,”android.intent.action.USER_REMOVED”

    这些广播被privileged权限所保护,大多数普通app并收不到这些广播。

  • “android.intent.action.TIME_SET”, ACTION_TIMEZONE_CHANGED, ACTION_NEXT_ALARM_CLOCK_CHANGED

    当时间、时区或闹钟有变化时,时钟应用会需要接收这些广播以更新时钟。

  • ACTION_LOCALE_CHANGED

    这个广播只会在定位变化的时候发送,并不会很频繁。有些应用会需要这些定位变化来更新数据。

  • ACTION_USB_ACCESSORY_ATTACHED,ACTION_USB_ACCESSORY_DETACHED,ACTION_USB_DEVICE_ATTACHED,ACTION_USB_DEVICE_DETACHED

    如果应用程序需要了解这些与USB相关的事件,那么除了注册广播之外,目前没有其他好的选择。

  • ACTION_CONNECTION_STATE_CHANGED,ACTION_CONNECTION_STATE_CHANGED,ACTION_ACL_CONNECTED,ACTION_ACL_DISCONNECTED

    如果应用收到这些蓝牙事件的广播,则用户体验不太可能会受到影响。

  • ACTION_CARRIER_CONFIG_CHANGED,TelephonyIntents.ACTION_SUBSCRIPTION_CHANGED,TelephonyIntents.SECRET_CODE_ACTION,ACTION_PHONE_STATE_CHANGED,ACTION_PHONE_ACCOUNT_REGISTERED,ACTION_PHONE_ACCOUNT_UNREGISTERED

    OEM电话应用程序可能需要接收这些广播。

  • LOGIN_ACCOUNTS_CHANGED_ACTION

    一些应用程序需要了解登录帐户的更改,以便它们可以为新帐户和更改的帐户设置计划的操作。

  • ACTION_ACCOUNT_REMOVED

    拥有帐户可见性的应用在删除帐户后会收到此广播。 如果这是应用程序需要执行的唯一帐户更改,则强烈建议应用程序使用此广播,而不要使用不建议使用的LOGIN_ACCOUNTS_CHANGED_ACTION

  • ACTION_PACKAGE_DATA_CLEARED

    仅当用户从“设置”中明确清除其数据时才发送,因此广播接收器不太可能严重影响用户体验。

  • ACTION_PACKAGE_FULLY_REMOVED

    某些应用可能需要在删除另一个软件包后更新其存储的数据; 对于这些应用,没有什么好办法可以注册此广播。

    Note: 其他与包相关的广播(例如,ACTION_PACKAGE_REPLACED)不受新限制。 这些广播非常普遍,因此可能会对性能产生影响,因此将其排除在外。

  • ACTION_NEW_OUTGOING_CALL

    响应用户发出呼叫而采取行动的应用需要接收此广播。

  • ACTION_DEVICE_OWNER_CHANGED

    该广播不是经常发送; 一些应用程序需要接收它,以便他们知道设备的安全状态已更改。

  • ACTION_EVENT_REMINDER

    由calendar provider发送,以将事件提醒发布到日历应用程序。 由于日历提供程序不知道日历应用程序是什么,因此此广播必须是隐式的。

  • ACTION_MEDIA_MOUNTED,ACTION_MEDIA_CHECKING,ACTION_MEDIA_UNMOUNTED,ACTION_MEDIA_EJECT,ACTION_MEDIA_UNMOUNTABLE,ACTION_MEDIA_REMOVED,ACTION_MEDIA_BAD_REMOVAL

    这些广播是由于用户与设备的物理交互(安装或删除存储卷)或作为启动初始化的一部分(随着可用卷被挂载)而发送的,因此它们并不常见,通常在用户的控制之下 。

  • SMS_RECEIVED_ACTION,WAP_PUSH_RECEIVED_ACTION

    SMS收件人apps依赖这些广播。

Android O问题的解决方案

检查在应用的清单中定义的BroadcastReceiver。 如果清单里为显式广播声明了接收器,则必须予以替换。 可能的解决方法包括:

  • 通过调用 Context.registerReceiver() 动态创建BroadcastReceiver而不是在清单中声明一个静态的BroadcastReceiver。

  • 使用JobSchedular检查条件是否会触发隐式广播。

  • 如果在单个进程中使用广播在应用程序组件之间进行通信,请切换为使用LocalBroadcastManager。

  • 如果使用广播在自己的多个进程中的应用程序组件之间进行通信,请切换为使用显式广播。

  • 如果你想接收系统发送的隐式广播(例如ACTION_PACKAGE_ADDED),建议保持targetSdkVersion为25或更低。

  • 如果非要发送隐式广播,则可以通过查找接收方并发送各个显式广播来突破禁令:

    private static void sendImplicitBroadcast(Context ctxt, Intent i) {
      PackageManager pm=ctxt.getPackageManager();
      List matches=pm.queryBroadcastReceivers(i, 0);
    
      for (ResolveInfo resolveInfo : matches) {
        Intent explicit=new Intent(i);
        ComponentName cn=
          new ComponentName(resolveInfo.activityInfo.applicationInfo.packageName,
            resolveInfo.activityInfo.name);
    
        explicit.setComponent(cn);
        ctxt.sendBroadcast(explicit);
      }
    }
    

官方文档建议:在大多数情况下,应用都可以使用 JobScheduler 克服这些限制。 这种方法允许应用安排其在未活跃运行时执行工作,不过仍能够使系统可以在不影响用户体验的情况下安排这些作业。 Android 8.0 提供针对 JobScheduler 的多项改进,让用户可以更轻松地使用计划作业取代 Service 和BroadcastReceiver: JobScheduler 改进。后面会整理一篇JobScheduler继续跟进这个问题。

参考

https://commonsware.com/blog/2017/04/11/android-o-implicit-broadcast-ban.html


文章作者: Wossoneri
版权声明: 本博客所有文章除特別声明外,均采用 CC BY-NC 4.0 许可协议。转载请注明来源 Wossoneri !
评论
  目录